﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Reflection;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace MvcImproved.Xml
{
	class MvcXmlSerializer
	{
		public enum SerializeAction
		{
			Serialize,
			Ignore
		}

		public SerializeAction DefaultAction = SerializeAction.Ignore;

		private Dictionary<System.Type, System.Type> _SerializeTypeOverride = new Dictionary<Type, Type>();
		public Dictionary<System.Type, System.Type> SerializeTypeOverride
		{
			get
			{
				return _SerializeTypeOverride;
			}
		}

		private static HashSet<Type> _TypeLookup = new HashSet<Type>(new System.Type[]{ typeof(XmlAttributeAttribute), typeof(XmlElementAttribute),
				typeof(XmlAnyAttributeAttribute), typeof(XmlAnyElementAttribute), typeof(XmlArrayAttribute),
				typeof(XmlArrayItemAttribute), typeof(XmlChoiceIdentifierAttribute), typeof(XmlEnumAttribute),
				typeof(XmlIgnoreAttribute), typeof(XmlIncludeAttribute), typeof(XmlNamespaceDeclarationsAttribute),
				typeof(XmlRootAttribute), typeof(XmlSchemaProviderAttribute), typeof(XmlSerializerAssemblyAttribute),
				typeof(XmlSerializerVersionAttribute), typeof(XmlTextAttribute), typeof(XmlTypeAttribute) });

		public MvcXmlSerializer(Type t)
		{
		}

		private class MemberXmlAttributes
		{
			public MemberInfo Member;
			public List<Attribute> Attributes = new List<Attribute>();

			public T AttributeType<T>() where T: Attribute
			{
				return Attributes.FirstOrDefault(a => a is T) as T;
			}
		}

		private MemberXmlAttributes ProcessTypeMember(object[] attributes, MemberInfo member)
		{
			MemberXmlAttributes currentMember = new MemberXmlAttributes() { Member = member };
			foreach (object attribute in attributes)
			{
				if (_TypeLookup.Contains(attribute.GetType()))
				{
					if (currentMember.Attributes == null)
						currentMember.Attributes = new List<Attribute>();
					currentMember.Attributes.Add((Attribute)attribute);
				}
			}

			return currentMember;
		}

		private void ProcessType(List<MemberXmlAttributes> memberList, Type type, Type buddyType, Func<Type, MemberInfo[]> memberLookup)
		{
			if (buddyType == null)
			{
				foreach (MemberInfo member in memberLookup(type))
				{
					MemberXmlAttributes xmlMember = ProcessTypeMember(member.GetCustomAttributes(true), member);
					memberList.Add(xmlMember);
				}
			}
			else
			{
				var buddyClassProperties = from member in memberLookup(type)
										   join buddyMember in memberLookup(buddyType) on member.Name equals buddyMember.Name
										   into joinedProperties
										   from finalMember in joinedProperties.DefaultIfEmpty(member)
										   select new { Override = finalMember, Source = member };

				foreach (var memberPair in buddyClassProperties)
				{
					MemberInfo memberToUse = (memberPair.Override != null) ? memberPair.Override : memberPair.Source;
					MemberXmlAttributes xmlMember = ProcessTypeMember(memberToUse.GetCustomAttributes(true), memberPair.Source);
					memberList.Add(xmlMember);
				}
			}
		}

		private static Type ExtractGenericInterface(Type queryType, Type interfaceType)
		{
			Func<Type, bool> matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType;
			return (matchesInterface(queryType)) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface);
		}

		private static Type ExtractInterface(Type queryType, Type interfaceType)
		{
			Func<Type, bool> matchesInterface = t => t == interfaceType;
			return (matchesInterface(queryType)) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface);
		}

		TypeConverter _StringConverter = TypeDescriptor.GetConverter(typeof(string));

		private void StartElement<T>(XmlWriter writer, object value, string memberName, T xmlAttribute, Func<T, string> nameAccess, Func<T, string> nsAccess, Func<T, string> typeAccess) where T : class
		{
			if (xmlAttribute != null)
			{
				string elementName = nameAccess(xmlAttribute);
				if (String.IsNullOrEmpty(elementName))
					elementName = memberName;
				writer.WriteStartElement(elementName, nsAccess(xmlAttribute));
				string dataType = typeAccess(xmlAttribute);
				if (! String.IsNullOrEmpty(dataType))
					writer.WriteAttributeString("xsi", "type", SchemaInstanceNs, dataType);
			}
			else
				writer.WriteStartElement(memberName);
			if (value == null)
				writer.WriteAttributeString("xsi", "nil", SchemaInstanceNs, "true");
		}

		private void WriteAttribute(XmlWriter writer, string memberName, XmlAttributeAttribute xmlAttribute, string value)
		{
			if (xmlAttribute != null)
			{
				string attributeName = xmlAttribute.AttributeName;
				if (String.IsNullOrEmpty(attributeName))
					attributeName = memberName;
				writer.WriteAttributeString(attributeName, xmlAttribute.Namespace, value);
			}
			else
				writer.WriteAttributeString(memberName, value);
		}

		private class XmlOutputMember
		{
			public MemberXmlAttributes Member;
			public object Value;
			public Type ValueType;
		}

		private void SerializeInstance(XmlWriter writer, object obj, Type type)
		{
			if (obj == null)
				return;

			Type buddyType = null;
			if (!SerializeTypeOverride.TryGetValue(type, out buddyType))
			{
				var metadataAttrib = type.GetCustomAttributes(typeof(MetadataTypeAttribute), true).OfType<MetadataTypeAttribute>().FirstOrDefault();
				if (metadataAttrib != null)
					buddyType = metadataAttrib.MetadataClassType;
			}

			List<MemberXmlAttributes> xmlMembers = new List<MemberXmlAttributes>();
			BindingFlags lookupFlags = BindingFlags.Instance | BindingFlags.Public;

			ProcessType(xmlMembers, type, buddyType, t => t.GetProperties(lookupFlags));
			ProcessType(xmlMembers, type, buddyType, t => t.GetFields(lookupFlags));

			List<XmlOutputMember> attributes = new List<XmlOutputMember>();
			List<XmlOutputMember> elements = new List<XmlOutputMember>();
			foreach (var xmlMember in xmlMembers)
			{
				if (xmlMember.AttributeType<XmlIgnoreAttribute>() != null)
					continue;
				else if ((DefaultAction == SerializeAction.Ignore) && (xmlMember.Attributes.Count == 0))
					continue;

				object value;
				Type valueType;
				if (xmlMember.Member.MemberType == MemberTypes.Property)
				{
					PropertyInfo propertyInfo = (PropertyInfo)xmlMember.Member;
					value = propertyInfo.GetValue(obj, null);
					valueType = (value != null) ? value.GetType() : propertyInfo.PropertyType;
				}
				else
				{
					FieldInfo fieldInfo = (FieldInfo)xmlMember.Member;
					value = fieldInfo.GetValue(obj);
					valueType = (value != null) ? value.GetType() : fieldInfo.FieldType;
				}

				if (value == null)
					continue;

				TypeConverter memberTypeConverter = TypeDescriptor.GetConverter(valueType);
				XmlAttributeAttribute xmlAttribute = xmlMember.AttributeType<XmlAttributeAttribute>();
				if ((valueType.IsValueType || (valueType == typeof(string)) || (xmlAttribute != null))
					&& (_StringConverter.CanConvertFrom(valueType) || memberTypeConverter.CanConvertTo(typeof(string))))
				{
					attributes.Add(new XmlOutputMember() { Member = xmlMember, Value = value, ValueType = valueType });
				}
				else
					elements.Add(new XmlOutputMember() { Member = xmlMember, Value = value, ValueType = valueType });
			}

			foreach (var member in attributes)
			{
				string stringValue;
				TypeConverter memberTypeConverter = TypeDescriptor.GetConverter(member.ValueType);
				if (memberTypeConverter.CanConvertTo(typeof(string)))
					stringValue = (string)memberTypeConverter.ConvertTo(member.Value, typeof(string));
				else
					stringValue = (string)_StringConverter.ConvertFrom(member.Value);
				XmlElementAttribute xmlElement = member.Member.AttributeType<XmlElementAttribute>();
				if (xmlElement != null)
				{
					StartElement(writer, member.Value, member.Member.Member.Name, xmlElement, e => e.ElementName, e => e.Namespace, e => e.DataType);
					writer.WriteString(stringValue);
					writer.WriteEndElement();
				}
				else
				{
					XmlAttributeAttribute xmlAttribute = member.Member.AttributeType<XmlAttributeAttribute>();
					WriteAttribute(writer, member.Member.Member.Name, xmlAttribute, stringValue);
				}
			}

			foreach (var member in elements)
			{
				Type enumerableType = ExtractInterface(member.ValueType, typeof(IEnumerable));
				if (enumerableType != null)
				{
					Type genericEnumerableType = ExtractInterface(member.ValueType, typeof(IEnumerable<>));

					XmlArrayAttribute arrayAttribute = member.Member.AttributeType<XmlArrayAttribute>();
					//TODO: use XmlArrayItem correctly
					XmlArrayItemAttribute arrayItemAttribute = member.Member.AttributeType<XmlArrayItemAttribute>();
					//TODO: conditions for element vs array
					XmlElementAttribute xmlElement = member.Member.AttributeType<XmlElementAttribute>();
					if (arrayAttribute != null)
						writer.WriteStartElement(arrayAttribute.ElementName, arrayAttribute.Namespace);
					Type collectionElementType = (genericEnumerableType != null) ? genericEnumerableType.GetGenericArguments()[0] : null;
					if (member.Value != null)
					{
						foreach (object element in (IEnumerable)member.Value)
						{
							Type elementType = (element != null) ? element.GetType() : collectionElementType;
							if (elementType != null)
							{
								if (arrayAttribute != null)
									StartElement(writer, member.Value, member.Member.Member.Name, arrayItemAttribute, e => e.ElementName, e => e.Namespace, e => e.DataType);
								else
									StartElement(writer, member.Value, member.Member.Member.Name, xmlElement, e => e.ElementName, e => e.Namespace, e => e.DataType);
								SerializeInstance(writer, element, elementType);
								writer.WriteEndElement();
							}
						}
					}
					if (arrayAttribute != null)
						writer.WriteEndElement();
				}
				else
				{
					XmlElementAttribute xmlElement = member.Member.AttributeType<XmlElementAttribute>();
					StartElement(writer, member.Value, member.Member.Member.Name, xmlElement, e => e.ElementName, e => e.Namespace, e => e.DataType);

					SerializeInstance(writer, member.Value, member.ValueType);

					writer.WriteEndElement();
				}
			}
		}

		private static string SchemaInstanceNs = "http://www.w3.org/2001/XMLSchema-instance";
		private static string SchemaNs = "http://www.w3.org/2001/XMLSchema";
		public void Serialize(XmlWriter writer, object obj)
		{
			Type type = obj.GetType();
			XmlRootAttribute rootAttribute = type.GetCustomAttributes(typeof(XmlRootAttribute), false).Cast<XmlRootAttribute>().FirstOrDefault();

			StartElement(writer, obj, type.Name, rootAttribute, e => e.ElementName, e => e.Namespace, e => e.DataType);

			writer.WriteAttributeString("xmlns", "xsi", null, SchemaInstanceNs);
			writer.WriteAttributeString("xmlns", "xsd", null, SchemaNs);

			SerializeInstance(writer, obj, type);

			writer.WriteEndElement();
			writer.Flush();
		}
	}
}
